/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.nageoffer.shortlink.admin.common.biz.user;

import com.alibaba.fastjson2.JSON;
import com.google.common.collect.Lists;
import com.nageoffer.shortlink.admin.common.convention.exception.ClientException;
import com.nageoffer.shortlink.admin.common.convention.result.Results;
import com.nageoffer.shortlink.admin.config.UserFlowRiskControlConfiguration;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Optional;

import static com.nageoffer.shortlink.admin.common.convention.errorcode.BaseErrorCode.FLOW_LIMIT_ERROR;

/**
 * 用户操作流量风控过滤器
 */
@Slf4j
@RequiredArgsConstructor
public class UserFlowRiskControlFilter implements Filter {

    private final StringRedisTemplate stringRedisTemplate;
    private final UserFlowRiskControlConfiguration userFlowRiskControlConfiguration;

    private static final String USER_FLOW_RISK_CONTROL_LUA_SCRIPT_PATH = "lua/user_flow_risk_control.lua";

    /**
     * 使用SneakyThrows注解来优雅地处理可能抛出的异常
     * 实现Filter接口，对请求进行过滤处理
     *
     * @param request Servlet请求对象，用于获取请求信息
     * @param response Servlet响应对象，用于向客户端发送响应
     * @param filterChain 过滤链对象，用于将请求传递给下一个过滤器或目标资源
     * @throws IOException 如果发生输入/输出错误
     * @throws ServletException 如果在执行过程中发生Servlet异常
     */
    @SneakyThrows
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        // 初始化Redis脚本对象，用于执行Lua脚本
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        // 设置脚本源为类路径下的指定Lua脚本文件
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(USER_FLOW_RISK_CONTROL_LUA_SCRIPT_PATH)));
        // 设置脚本执行结果类型为Long
        redisScript.setResultType(Long.class);
        // 获取当前用户名，如果不存在，则使用"other"作为默认值
        String username = Optional.ofNullable(UserContext.getUsername()).orElse("other");
        Long result;
        try {
            // 执行Redis Lua脚本，参数包括用户名和时间窗口大小
            result = stringRedisTemplate.execute(redisScript, Lists.newArrayList(username), userFlowRiskControlConfiguration.getTimeWindow());
        } catch (Throwable ex) {
            // 如果执行脚本出错，记录错误日志并返回错误响应
            log.error("执行用户请求流量限制LUA脚本出错", ex);
            returnJson((HttpServletResponse) response, JSON.toJSONString(Results.failure(new ClientException(FLOW_LIMIT_ERROR))));
            return;
        }
        // 如果脚本执行结果为空或超过最大访问次数限制，返回错误响应
        if (result == null || result > userFlowRiskControlConfiguration.getMaxAccessCount()) {
            returnJson((HttpServletResponse) response, JSON.toJSONString(Results.failure(new ClientException(FLOW_LIMIT_ERROR))));
            return;
        }
        // 如果请求通过检查，继续执行过滤链中的下一个过滤器或目标资源
        filterChain.doFilter(request, response);
    }

    /**
     * 将指定的JSON字符串作为HTTP响应返回给客户端
     * 此方法用于设置HTTP响应的内容类型和字符编码，并将JSON字符串写入响应体中
     * 选择"text/html; charset=utf-8"作为内容类型是因为在某些情况下，例如跨域请求时，
     * 使用"application/json"可能会导致浏览器不允许访问响应内容，而使用"text/html"则通常不受此限制
     *
     * @param response Servlet响应对象，用于向客户端发送数据
     * @param json     要返回给客户端的JSON字符串
     * @throws Exception 如果在写入响应过程中发生错误
     */
    private void returnJson(HttpServletResponse response, String json) throws Exception {
        // 设置响应的字符编码为UTF-8，确保中文字符可以正确显示
        response.setCharacterEncoding("UTF-8");
        // 设置响应的内容类型为文本/html，charset=utf-8确保编码一致
        response.setContentType("text/html; charset=utf-8");
        // 使用try-with-resources语句自动管理资源，确保在操作完成后关闭PrintWriter
        try (PrintWriter writer = response.getWriter()) {
            // 将JSON字符串写入响应体中
            writer.print(json);
        }
    }
}
